using System;
using System.Drawing;
using System.Drawing.Drawing2D;


namespace Camalon.Drawing
{
	/// <summary>
	/// Summary description for QuickWarper.
	/// </summary>
	public class QuickWarper
	{
		const float EPSILON = 1.0e-18f;

		Matrix _matrix = new Matrix();
		float _a0, _a1, _a2;
		float _b0, _b1, _b2, _b3;
		float _c1;

		float _a3;	// bilinear mode only
		float _c0;	// perspective mode only

		WarpMode _wm = WarpMode.Perspective;
		private PointF[] _destPoints = null;

		// Construct a QWarper object.
		// The parameters are identical to the parameters of the GraphicsPath::Warp() member
		// function, the only difference being that destPoints always points to (at least)
		// four points. 'Three point warp' is not supported.
		public QuickWarper(PointF[] destPoints, RectangleF srcRect, WarpMode mode)
		{
			_wm = mode;
			_destPoints = destPoints;

			if(_wm != WarpMode.Bilinear && !CanWarpPerspective( destPoints)) _wm = WarpMode.Bilinear;

			PointF center = new PointF(srcRect.X + srcRect.Width / 2.0f, srcRect.Y + srcRect.Height / 2.0f);

			// translate so that srcRect is centered at origin
			_matrix.Translate(- center.X, - center.Y);

			// Scale srcRect to square with sides 2.
			// Corners are now (-1, -1), (1, -1), (1, 1), (-1, 1).
			// These will be mapped to destPoints.
			_matrix.Scale(2.0f / srcRect.Width, 2.0f / srcRect.Height, MatrixOrder.Append);

			// In warping, we first apply an affine transformation, using mat.
			// Next, the result is warped, using warping functions. Several factors
			// of these functions are pre-calculated in the constructor.
			CalculateFactors(destPoints);

		}


		private void CalculateFactors(PointF[] destPoints)
		{
			// In warping, we first apply an affine transformation, using mat.
			// Next, the result is warped, using warping functions. Several factors
			// of these functions are pre-calculated.

			if (_wm == WarpMode.Perspective)
			{
				// In perspective mode, the warping functions are:
				//	x' = (_a0 + _a1 x + _a2 y) / (_c0 x + _c1 y + 1)
				//	y' = (_b0 + _b1 x + _b2 y) / (_c0 x + _c1 y + 1)
				//
				// The following calculates the factors a#, b# and c#.
				// The formula's are derived by:
				// 1. substituting the corners of the normalized square for (x, y);
				// 2. substituting the corresponding destPoints for (x', y');
				// 3. solving the resulting set of eight equations, with the factors as unknowns.
				// It all amounts to a lot of old fashioned calculus by hand...

				// First a few intermediate values are calculated
				float px = destPoints[0].X + destPoints[1].X - destPoints[2].X - destPoints[3].X; 
				float qx = destPoints[0].X - destPoints[1].X + destPoints[2].X - destPoints[3].X; 
				float rx = destPoints[0].X - destPoints[1].X - destPoints[2].X + destPoints[3].X; 

				float py = destPoints[0].Y + destPoints[1].Y - destPoints[2].Y - destPoints[3].Y; 
				float qy = destPoints[0].Y - destPoints[1].Y + destPoints[2].Y - destPoints[3].Y; 
				float ry = destPoints[0].Y - destPoints[1].Y - destPoints[2].Y + destPoints[3].Y; 

				float num = px * qy - qx * py;

				// Avoid divide by zero (i.e.Overflow exception)
				if (num == 0f || System.Math.Abs(num) < EPSILON) num = 1f;
				if (px == 0f ||System.Math.Abs(px) < EPSILON )   px  = 1f;

				// The final factors for the warping functions
				_c1 = (px * ry - rx * py) / num;
				_c0 = (rx - qx * _c1) / px;

				_a1 = ((destPoints[0].X + destPoints[1].X) * _c0 + (destPoints[0].X - destPoints[1].X) * (_c1 - 1.0f)) / 2.0f;
				_a0 = ((destPoints[1].X + destPoints[3].X) * (_c0 + 1.0f) + (- destPoints[1].X + destPoints[3].X) * _c1) / 2.0f - _a1;
				_a2 = ((- destPoints[2].X + destPoints[3].X) * _c0 + (destPoints[2].X + destPoints[3].X) * (_c1 + 1.0f)) / 2.0f - _a0;

				_b1 = ((destPoints[0].Y + destPoints[1].Y) * _c0 + (destPoints[0].Y - destPoints[1].Y) * (_c1 - 1.0f)) / 2.0f;
				_b0 = ((destPoints[1].Y + destPoints[3].Y) * (_c0 + 1.0f) + (- destPoints[1].Y + destPoints[3].Y) * _c1) / 2.0f - _b1;
				_b2 = ((- destPoints[2].Y + destPoints[3].Y) * _c0 + (destPoints[2].Y + destPoints[3].Y) * (_c1 + 1.0f)) / 2.0f - _b0;
			}
			else
			{
				// In bilinear mode, the warping functions are:
				//	x' = _a0 + _a1 x y + _a2 x + _a3 y
				//	y' = _b0 + _b1 x y + _b2 x + _b3 y
				_a0 = (  destPoints[0].X + destPoints[1].X + destPoints[2].X + destPoints[3].X) / 4.0f; 
				_a1 = (  destPoints[0].X - destPoints[1].X - destPoints[2].X + destPoints[3].X) / 4.0f; 
				_a2 = (- destPoints[0].X + destPoints[1].X - destPoints[2].X + destPoints[3].X) / 4.0f; 
				_a3 = (- destPoints[0].X - destPoints[1].X + destPoints[2].X + destPoints[3].X) / 4.0f; 

				_b0 = (  destPoints[0].Y + destPoints[1].Y + destPoints[2].Y + destPoints[3].Y) / 4.0f; 
				_b1 = (  destPoints[0].Y - destPoints[1].Y - destPoints[2].Y + destPoints[3].Y) / 4.0f; 
				_b2 = (- destPoints[0].Y + destPoints[1].Y - destPoints[2].Y + destPoints[3].Y) / 4.0f; 
				_b3 = (- destPoints[0].Y - destPoints[1].Y + destPoints[2].Y + destPoints[3].Y) / 4.0f; 
			}
		}
		

		public void WarpPoints(PointF[] points)
		{
			// Final warping is in two loops:
			// 1. apply affine transformation to the normalized square around the origin (identical in both modes);
			// 2. apply warping functions, specific for each mode.
			// The fact that the warping functions are non-linear makes it impractical 
			// and uneconomical to combine the affine transformation and the warping functions
			// in one operation.

			_matrix.TransformPoints(points);

			if (_wm == WarpMode.Perspective)
			{
				for (int i = 0; i < points.Length; i++)
				{
					float x = points[i].X;
					float y = points[i].Y;
					float num = _c0 * x + _c1 * y + 1.0f;

					//Prevent divide by zero (i.e.Overflow exception)
					if (num == 0f || System.Math.Abs(num) < EPSILON ) num = 1f;

					points[i].X = (_a0 + _a1 * x + _a2 * y) / num;
					points[i].Y = (_b0 + _b1 * x + _b2 * y) / num;
				}
			}
			else
			{
				for (int i = 0; i < points.Length; i++)
				{
					float x = points[i].X;
					float y = points[i].Y;
					float xy = x * y;
					points[i].X = _a0 + _a1 * xy + _a2 * x + _a3 * y;
					points[i].Y = _b0 + _b1 * xy + _b2 * x + _b3 * y;
				}
			}

		}


		public GraphicsPath WarpPath(GraphicsPath path)
		{
			byte[] pd			= path.PathTypes;
			PointF[] pnts		= path.PathPoints;
			WarpPoints(pnts);
			path = new GraphicsPath(pnts, pd, path.FillMode);
			return path;
		}


		public static bool CanWarpPerspective( PointF[] destinationPoints )
		{
			PointF tl	= destinationPoints[0];
			PointF tr	= destinationPoints[1];
			PointF bl	= destinationPoints[2];
			PointF br	= destinationPoints[3];

			PointF tlbrCenter	= RectangleUtil.CenterPoint( new PointF[]{ tl, br});
			float phase	= GetAngle( tlbrCenter, br);	if( phase < 0) phase +=360;
			//Bottomleft must stay between start and end angle
			float angle_bl		= GetAngle( tlbrCenter, bl)-phase;if( angle_bl < 0) angle_bl +=360;
			if( angle_bl == 0 || angle_bl >= 180f) return false;
			float angle_tr = GetAngle( tlbrCenter, tr)-phase; if( angle_tr < 0) angle_tr +=360;
			if( angle_tr == 0 || angle_tr <= 180f) return false;

			PointF bltrCenter	= RectangleUtil.CenterPoint( new PointF[]{ bl, tr});
			phase	= GetAngle( bltrCenter, bl);	if( phase < 0) phase +=360;
			//Bottomleft must stay between start and end angle
			float angle_tl		= GetAngle( bltrCenter, tl)-phase;if( angle_tl < 0) angle_tl +=360;
			if( angle_tl == 0 || angle_tl >= 180) return false;
			float angle_br = GetAngle( bltrCenter, br)-phase; if( angle_br < 0) angle_br +=360;
			if( angle_br == 0 || angle_br <= 180) return false;
			
			return true;

		}
		
		private static float GetAngle( PointF rotatingPoint, PointF anchorPoint )
		{
			//Recalculate radius
			//------------------------------------------------
			PointF pt = new PointF (rotatingPoint.X - anchorPoint.X , rotatingPoint.Y  - anchorPoint.Y  ); 
			double radian = Math.Atan2( pt.Y, pt.X );
			double angle = radian * (180.0/Math.PI);
			return (float)(angle) + 180;
		}

	}
}
